package com.bxait.toymail.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.bxait.com.utils.PageUtils;
import com.bxait.com.utils.Query;
import com.bxait.com.vo.product.CateLog2Vo;
import com.bxait.toymail.product.service.CategoryBrandRelationService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bxait.toymail.product.dao.CategoryDao;
import com.bxait.toymail.product.entity.CategoryEntity;
import com.bxait.toymail.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    private Map<String,Object> cache = new HashMap<>();

    // 这地方有个坑,Resource是根据名称来进行注入的,而RedisAutoConfiguration自动放的那个bean的名称就叫stringRedisTemplate
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    RedissonClient redisson;

    @Resource
    private CategoryBrandRelationService categoryBrandRelationService;


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * @param
     * @==============History===============<br/>
     * @Description // TODO  获取所以的商品分类数据
     * @Date 2023/3/18 2023/3/18
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return []
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        // 1.获取所以的分类数据
        List<CategoryEntity> entities
                = baseMapper.selectList(null);
        List<CategoryEntity> collect = entities.stream()
        // 2.获取所有的一级分类
                .filter(item -> item.getParentCid() == 0)
                .map(menu -> {
        // 3.给所有的一级分类设置子分类
                    menu.setChildren(getChildren(menu, entities));
                    return menu;
                })
        // 4.给一级分类菜单按照商品分类的sort进行排序
                .sorted(Comparator.comparingInt(menu->(menu.getSort() == null ? 0:menu.getSort())))
                .collect(Collectors.toList());
        return collect;
    }



    /**
     * @param root
     * @param all
     * @==============History===============<br/>
     * @Description // TODO  给当前一级分类设置子分类
     * @Date 2023/3/18 2023/3/18
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return [com.bxait.toymail.product.entity.CategoryEntity, java.util.List<com.bxait.toymail.product.entity.CategoryEntity>]
     */
    private  List<CategoryEntity> getChildren(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> collect = all.stream()
                // filter过滤出当前一级分类的子分类
                .filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
                // map 给这个收集好的子分类,如果这个子分类还有子分类,递归设置子分类
                .map(item -> {
                    item.setChildren(getChildren(item, all));
                    return item;
                })
                // sorted 按照商品分类的sort进行排序
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null?0:menu.getSort())))
                .collect(Collectors.toList());
        return collect;
    }



    /**
     * @param cateLogId 三级分类的id
     * @param paths     当前三级分类的全路径
     * @==============History===============<br/>
     * @Description // TODO  返回当前三级分类的全路径
     * @Date 2023/3/15 23:29
     * @Author <a href="qq:2277411582@qq.com">chen</a>
     * @return java.util.List<java.lang.Long>
     */
    @Override
    public List<Long> findParentPath(Long cateLogId, List<Long> paths){

        if(cateLogId != 0){
            // 1.进入的时候先收集当前分类的id
            paths.add(cateLogId);
            // 2.获取当前分类的实体
            CategoryEntity entity = this.getById(cateLogId);
            // 3.如果当前分类还有父分类,递归查询
            if(entity.getParentCid() != 0){
                findParentPath(entity.getParentCid(),paths);
            }
            // 4.递归调用结束,反转这个分类的全路径返回
            Collections.reverse(paths);
        }

        return paths;
    }

    /**
     * @param
     * @==============History===============<br/>
     * @Description // TODO  返回所以的一级分类
     * @Date 2023/3/26 2023/3/26
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return []
     */
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevelOneCateGory() {
        QueryWrapper<CategoryEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_cid",0);
        List<CategoryEntity> categoryEntities = baseMapper.selectList(wrapper);
        return categoryEntities;
    }

    /**
     * @param category
     * @==============History===============<br/>
     * @Description // TODO  更新商品分类信息,需要基连的修改商品品牌关联表中的信息
     * @Date 2023/4/8 2023/4/8
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return [com.bxait.toymail.product.entity.CategoryEntity]
     */
    @CacheEvict(value = "category",allEntries = true) // 失效模式
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        // 更新商品分类信息
        this.updateById(category);
        // 更新商品品牌分类关联信息
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }


    /**
     * @param
     * @==============History===============<br/>
     * @Description // TODO  组装所有的分类信息进行返回
     * @Date 2023/3/26 2023/3/26
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return []
     */
    @Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<CateLog2Vo>> getCatalogJson() {

        Map<String, List<CateLog2Vo>> map = null;
//         第一个版本  version 1.0 在数据库查一级分类一级分类有二级分类继续查询数据库 二级分类有三级分类继续查询数据库
//          map = getCatalogJsonVersion1();

//         第二个版本,version 2.0 只查一次数据库,封装方法返回数据
//         与数据库只进行一次交互,在组装出所以的商品分类信息
//             map = getCatalogJsonVersion2();

//         第三个版本,version 3.0  使用本地缓存存放分类数据,本地缓存中有就是用本地缓存中的数据,没有查询数据库
//          产生的问题:
//          问题一:本地缓存相当于只是在本地保存了一个副本,放在了jvm中,跟我们的项目放在一起。如果我们的服务是一个单台机器,这样来缓存是没有问题的而且很快
//          如果服务有多台,第一次请求负载到A机器,那么A机器就会缓存一份数据到自己本地。下一次请求如果能负载到A机器那就可以从缓存中取走数据。
//          但是如果下一次请求负载到了B机器,那么B机器就会查询一次数据库然后将数据缓存到自己的机器上,在负载到C机器,那么C机器也会查询一次数据库在缓存到自己的机器上
//          问题二:当我们修改了数据库中的数据时,我们没有办法将每一个微服务中的本地缓存都修改。
//             map = getCatalogJsonVersion3();

//         第四个版本,version 4.0  使用缓存中间键(redis)来存放缓存数据,这样的话,每一个微服务都是直接在缓存中获取数据,每一个微服务都来共享这个缓存中间件,
//         并且当数据库中的数据进行了修改时,可以同步的将缓存中间键
//         中的数据进行修改,这样最终每一个微服务获取到的缓存数据就和数据库中的都是一致的。
//               map = getCatalogJsonVersion4();

//         分布式环境下使用缓存产生的问题,缓存失效产生的问题
//         缓存不命中产生的问题
//         问题1(缓存穿透):流量高峰的时候,百万请求进来缓存中没有,查询数据库,大量服务压垮数据库。缓存没有起到作用
//                       查询一个一定不存在的数据,会将压力直接给到数据库;解决方案:将数据库查询到的空结果(null,false...)也进行缓存,并加入短暂过期时间
//         问题2(缓存雪崩):当我们给缓存中放了很多缓存数据,我们存放缓存的时候,给每一个都指定了过期时间,在某一个时间大面积的缓存过期(过期)
//                       请求全部转发到数据库,数据库压力过大雪崩;解决方案:我们在存放缓存数据的时候,为每一个缓存的数据设置随机的过期时间
//         问题3(缓存击穿):对于一些热点的key,访问频率很高 在某一个时间被高并发的访问,如果这个key在大量请求来临的时候正好失效,请求全部压倒数据库,
//                       数据库压力过大,服务崩溃无响应;
//                       解决方案:加锁 大量并发下只让一个请求去查数据库,其他请求等待,获取到数据后放入缓存释放锁,其他请求再次获得锁得时候缓存已经有数据了
//                       无需在查询数据库。
//              map = getCatalogJsonVersion5();

//         第六个版本,version 6.0 使用redis去抢锁,实现分布式锁
//              map =  getCatalogJsonVersion6();

//         第七个版本,version 7.0 使用springCache
                map =  getCatalogJsonVersion7();

//         第八个版本,version 8.0 使用redisson
//              map =  getCatalogJsonVersion8();


        return map;
    }





    // 组装所有的分类信息,version 1.0  (在数据库查一级分类一级分类有二级分类继续查询数据库 二级分类有三级分类继续查询数据库)
    protected Map<String, List<CateLog2Vo>> getCatalogJsonVersion1(){
        // 1.先获取所以的一级分类,在给一级分类组装二级分类
        List<CategoryEntity> catalogJsonL1 = getLevelOneCateGory();

        // 2.将每一个一级分类进行组装返回一个map
        Map<String, List<CateLog2Vo>> map = catalogJsonL1
                .stream()
                .collect(Collectors.toMap(
                        l1key -> l1key.getCatId().toString(),
                        l1value -> {
                            List<CateLog2Vo> cateLog2VoList = null;
                            // 2.1获取当前一级分类的二级分类
                            List<CategoryEntity> catalogJsonL2 = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l1value.getCatId()));
                            // 2.2当前一级分类的二级分类是有东西的,来收集二级分类
                            if(catalogJsonL2 != null && catalogJsonL2.size() > 0){
                                cateLog2VoList = catalogJsonL2.stream().map(l2 -> {
                                    CateLog2Vo cateLog2Vo = new CateLog2Vo();
                                    // 设置一级分类
                                    cateLog2Vo.setCatalog1Id(l1value.getCatId().toString());
                                    // 设置二级分类的信息
                                    cateLog2Vo.setId(l2.getCatId().toString());
                                    cateLog2Vo.setName(l2.getName());
                                    // 设置三级分类信息
                                    List<CategoryEntity> catalogJsonL3 = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
                                    List<CateLog2Vo.CateLog3Vo> cateLog3VoList = null;
                                    // 2.3当前二级分类的三级分类是有东西的,来收集三级分类
                                    if(catalogJsonL3 != null &&  catalogJsonL3.size() > 0){
                                        cateLog3VoList = catalogJsonL3.stream().map(l3 -> {
                                            CateLog2Vo.CateLog3Vo cateLog3Vo = new CateLog2Vo.CateLog3Vo();
                                            // 设置三级分类的二级父id
                                            cateLog3Vo.setCatalog2Id(l2.getCatId().toString());
                                            // 设置三级分类的信息
                                            cateLog3Vo.setId(l3.getCatId().toString());
                                            cateLog3Vo.setName(l3.getName());
                                            return cateLog3Vo;
                                        }).collect(Collectors.toList());
                                    }
                                    cateLog2Vo.setCatalog3List(cateLog3VoList);
                                    return cateLog2Vo;
                                }).collect(Collectors.toList());
                            }

                            return cateLog2VoList;
                        }));
        return map;
    }


    // 组装所有的分类信息,version 2.0  (与数据库只进行一次交互,在组装出所以的商品分类信息)
    protected Map<String, List<CateLog2Vo>> getCatalogJsonVersion2(){
        // 获取所以的商品分类信息
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        // 获取所以的一级分类数据
        List<CategoryEntity> catalogJsonL1 = getCatalogByParentCid(categoryEntities, 0L);
        Map<String, List<CateLog2Vo>> collect = catalogJsonL1
                .stream()
                .collect(Collectors.toMap(
                        l1key -> l1key.getCatId().toString(),
                        l1value -> {
                            List<CateLog2Vo> cateLog2VoList = new ArrayList<>();
                            List<CategoryEntity> catalogJsonL2 = getCatalogByParentCid(categoryEntities, l1value.getCatId());
                            if(catalogJsonL2 != null && catalogJsonL2.size() > 0){
                                cateLog2VoList = catalogJsonL2.stream().map(l2 -> {
                                    CateLog2Vo cateLog2Vo = new CateLog2Vo();
                                    // 设置一级分类
                                    cateLog2Vo.setCatalog1Id(l1value.getCatId().toString());
                                    // 设置二级分类的信息
                                    cateLog2Vo.setId(l2.getCatId().toString());
                                    cateLog2Vo.setName(l2.getName());
                                    // 给二级分类设置三级分类
                                    List<CategoryEntity> catalogJsonL3 = getCatalogByParentCid(categoryEntities, l2.getCatId());

                                    List<CateLog2Vo.CateLog3Vo> cateLog3VoList  =  new ArrayList<>();
                                    if(catalogJsonL3 != null && catalogJsonL3.size() > 0){
                                        cateLog3VoList = catalogJsonL3.stream().map(l3 -> {
                                            CateLog2Vo.CateLog3Vo cateLog3Vo = new CateLog2Vo.CateLog3Vo();
                                            // 设置三级分类的二级父id
                                            cateLog3Vo.setCatalog2Id(l2.getCatId().toString());
                                            // 设置三级分类的信息
                                            cateLog3Vo.setId(l3.getCatId().toString());
                                            cateLog3Vo.setName(l3.getName());
                                            return cateLog3Vo;
                                        }).collect(Collectors.toList());
                                    }
                                    cateLog2Vo.setCatalog3List(cateLog3VoList);
                                    return cateLog2Vo;
                                }).collect(Collectors.toList());

                            }
                            return cateLog2VoList;
                        }
                ));
        return collect;
    }

    // 组装所有的分类信息,version 3.0  (使用本地缓存,缓存中有直接取出缓存中的数据,与数据库只进行一次交互,在组装出所以的商品分类信息)
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion3() {
        Map<String, List<CateLog2Vo>> catalogJson
                = (Map<String, List<CateLog2Vo>>) cache.get("catalogJson");
        // 如果缓存中有数据直接返回缓存中的数据
        if(catalogJson != null){
            return catalogJson;
        }
        // 缓存中没有查询数据库,获取数据并封装,然后将封装号的数据保存缓存中
        Map<String, List<CateLog2Vo>> map =
                                getCatalogJsonVersion2();
        cache.put("catalogJson",map);
        return map;
    }

    // 组装所有的分类信息,version 4.0  (使用缓存中间件(redis),缓存中有直接取出缓存中的数据,与数据库只进行一次交互,在组装出所以的商品分类信息)
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion4(){
        // 给缓存中放json字符串,获取到的json我们也需要转换为能用的对象类型;[序列化与反序列化]
        // 1.从redis中获取分类数据
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        // 2.如果缓存没中有数据查询数据库返回分类数据,并将结果保存在缓存中
        if(StringUtils.isEmpty(catalogJson)){
            // 缓存中没有查询数据库,获取数据并封装,然后将封装号的数据保存缓存中
            Map<String, List<CateLog2Vo>> map =  getCatalogJsonVersion2();
            // 将查出的数据转换为json保存在redis中间件中,保存为json的好处是,跨语言跨平台大家都可以解析
            String json = JSON.toJSONString(map);
//            stringRedisTemplate.opsForValue().set("catalogJson",json);
            // version 5.0  设置过期时间为1天
            stringRedisTemplate.opsForValue().set("catalogJson",json,1, TimeUnit.DAYS);
            return map;
        }
        // 将json转换为我们能用的数据类型
        Map<String, List<CateLog2Vo>> mapResult = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<CateLog2Vo>>>() {
        });
        return mapResult;
    }

    // 组装所有的分类信息,version 5.0  (使用缓存中间件(redis),解决缓存高并发情况下出现得问题:1.缓存穿透;2.缓存雪崩;3.缓存击穿;)
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion5(){
        /*
         * 1.空结果缓存:解决缓存穿透问题
         * 2.设置过期时间(加随机值):解决缓存雪崩问题
         * 3.加锁:解决缓存击穿问题
         */
        // 加锁
        Map<String, List<CateLog2Vo>> catalogJson = null;
        // 0.先看缓存中是否缓存了数据,有直接返回,没有加锁查询数据库放入缓存返回
        String catalogJsonString = stringRedisTemplate.opsForValue().get("catalogJson");
        if(!StringUtils.isEmpty(catalogJsonString)){
            catalogJson =
                    JSON.parseObject(catalogJsonString, new TypeReference<Map<String, List<CateLog2Vo>>>() {});
            return catalogJson;
        }

        // 1.只要是一把锁,就能锁住需要这把锁得所以线程
        // synchronized (this) : springBoot所以的组件在容器中都是单例的。是可以锁住的
        // 得到锁之后,先看缓存中是否有数据,缓存有数据就不查数据库直接返回
        // 加synchronized产生得问题,在单体服务中加this锁住当前实例对象(一个容器),没有问题。但是在分布式环境下每个锁只能锁住自己的进程,本地锁不住所有的服务
        synchronized (this){
            catalogJson = getCatalogJsonVersion4();
        }

        // 2.加分布式锁(分布锁重,没有本地锁快synchronized,lock)

        return catalogJson;
    }

    // 组装所有的分类信息,version 6.0  (使用缓存中间件(redis) 去redis中占坑,设置过期时间,解决缓存高并发情况下出现得问题:1.缓存穿透;2.缓存雪崩;3.缓存击穿;)
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion6() {
        /*
         * 1.空结果缓存:解决缓存穿透问题
         * 2.设置过期时间(加随机值):解决缓存雪崩问题
         * 3.加锁:解决缓存击穿问题
         */
        Map<String, List<CateLog2Vo>> catalogJson = new HashMap<>();
        // 0.先看缓存中是否缓存了数据,有直接返回,没有加锁查询数据库放入缓存返回
        String catalogJsonString = stringRedisTemplate.opsForValue().get("catalogJson");
        if(!StringUtils.isEmpty(catalogJsonString)){
            catalogJson =
                    JSON.parseObject(catalogJsonString, new TypeReference<Map<String, List<CateLog2Vo>>>() {});
            return catalogJson;
        }
        // 1.使用redis加分布式锁,设置锁名叫lock,在redis中锁名为lock
        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if(lock){
             stringRedisTemplate.opsForValue().set("uuid",uuid);
            try {
                catalogJson = getCatalogJsonVersion2();
                // 将数据库查询出来的数据放一份到缓存中
                String json = JSON.toJSONString(catalogJson);
                stringRedisTemplate.opsForValue().set("catalogJson",json);
            }finally {
                // 将数据缓存到redis之后,同步的删锁,删锁和验锁需要是一个原子性操作
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Long delLock = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            return catalogJson;
        }else {
            // 获取锁失败,递归获取(自旋),这样锁就会有点重,
            try {
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatalogJsonVersion6();
        }
    }

    // 组装所有的分类信息,version 7.0  使用分布式缓存中间件
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion7(){
        return getCatalogJsonVersion2();
    }

    // 组装所有的分类信息,version 8.0  使用redisson操作锁
    private Map<String, List<CateLog2Vo>> getCatalogJsonVersion8(){
        // 1、锁的名字。 锁的粒度，越细越快。
        // 锁的粒度：具体缓存的是某个数据，11-号商品；  product-11-lock product-12-lock   product-lock
        RLock lock = redisson.getLock("CatalogJson-lock");
        lock.lock();
        Map<String, List<CateLog2Vo>> catalogJson;
        try {
            catalogJson = getCatalogJsonVersion2();
        } finally {
            lock.unlock();
        }
      return catalogJson;
    }

    // 返回集合中指定的元素
    protected List<CategoryEntity> getCatalogByParentCid(List<CategoryEntity> categoryEntities,Long parentCid){
        // 从已知的分类数据中,返回指定数据
        List<CategoryEntity> collect = categoryEntities.stream()
                .filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
        return collect;
    }
}